Skip to content

Identity-async safety: thread-scoped HoistIdentity cache + propagation#564

Draft
lbwexler wants to merge 3 commits into
developfrom
hoistIdentity
Draft

Identity-async safety: thread-scoped HoistIdentity cache + propagation#564
lbwexler wants to merge 3 commits into
developfrom
hoistIdentity

Conversation

@lbwexler
Copy link
Copy Markdown
Member

Summary

Phase 1 of docs/planning/identity-async-safety.md. Makes identity safe to read on threads that outlive the originating HTTP request (async continuations, auto-instrumented spans, cluster tasks) by moving the source of truth from the live request/session to a thread-scoped HoistIdentity cache, populated lazily on request threads and propagated explicitly across async boundaries.

Fixes the IllegalStateException: The request object has been recycled thrown by TraceService.createSpanIdentityService.getAuthUsernamerequest.getSession() (and the same shape via TagSpanProcessor.onStart, TrackService.parseSubmittedEntry).

Changes

  • New HoistIdentity (immutable POGO: username, authUsername)
  • New IdentityPropagatingPromiseFactory — installed at startup, propagates identity into Grails task {} workers
  • IdentityService refactored to a single ThreadLocal<HoistIdentity>; accessors read cache; mutators update cache + session in lock-step; getSessionIfExists catches IllegalStateException from recycled facades
  • HoistFilter clears the cache in finally to prevent leakage on pooled threads
  • ClusterTask installs/clears via the unified cache
  • TrackService / browser.Utils use a safeHeader helper so observability reads tolerate recycled facades

Out of scope

Phase 2 (eliminate live-request propagation into workers via a runDetached sibling to runAsync) is optional and not included here. See the plan doc.

Test plan

  • Build clean (./gradlew assemble — confirmed locally)
  • Manual: trigger original repro path (span/track from post-response async work); confirm no IllegalStateException
  • Manual: impersonation still works (apparent vs auth user surfaces correctly)
  • Manual: regular login/logout flow
  • Manual: cluster task identity propagation
  • Unit/integration tests deferred — recommend adding before merge

🤖 Generated with Claude Code

lbwexler and others added 3 commits May 19, 2026 07:33
…1 plan doc

Completes the new-file portion of the identity/async-safety changes already
committed in 4383aa4. See docs/planning/identity-async-safety.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, consolidations

- Single install method `installThreadIdentity(HoistIdentity)` (null = clear) plus typed
  installers `installIdentityFromRequest` and `installIdentityFromWebSocketSession`.
- HoistWebSocketHandler now installs identity around each lifecycle callback, so message
  handlers and channel construction can use the standard `identityService` accessors.
- Dropped lazy session-read from accessors — they read the thread cache directly. Removed
  `findAuthUser`, `getSessionIfExists`, `findHoistUser`, `cleanupThreadIdentity`, and the
  now-redundant legacy `threadUsername`/`threadAuthUsername` plumbing.
- Consolidated mutators around private `setIdentity` / `clearIdentity` helpers so session
  writes happen in exactly one place.
- Made `AUTH_USER_KEY` / `APPARENT_USER_KEY` private; `HoistWebSocketChannel` reaches
  identity through the service rather than session-attribute key constants.
- CHANGELOG entry added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant